﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ADONETDataServicesExtension.Utils;
using System.Runtime.InteropServices;
using System.Data.Services;
using System.Linq.Expressions;
using ADONETDataServicesExtension.Client;
using ADONETDataServicesExtension.Parsing;
using System.Reflection;
using System.Globalization;
using ADONETDataServicesExtension.Providers;
using System.Collections;
using System.Data.Objects;
using ADONETDataServicesExtension.BLLUtils;
using System.Data.Objects.DataClasses;
using System.Web;

namespace ADONETDataServicesExtension
{
    [StructLayout(LayoutKind.Sequential)]
    public class RequestQueryProcessor// : FrameworkEncapsulatedType<object> I don't know why but I have an exception when I try to inherit of FrameworkEncapsulatedType here
    {
        internal const string FrameworkTypeName = "System.Data.Services.RequestQueryProcessor";

        private object _requestQueryProcessor;

        private readonly IDataService _service;
        private readonly RequestDescription _description;
        private readonly bool _filterQueryApplicable;
        private readonly bool _setQueryApplicable;
        private List<ExpandSegmentCollection> _expandPaths;
        private List<List<string>> _expandPathsAsText;
        private bool _orderApplied;
        private IQueryable _query;
        private ObjectQuery _efObjectQuery;
        private IEnumerable _queryResults;
        private IEFEntitiesEncapsulatedContext _dataSource = null;

        private RequestQueryProcessor(IDataService service, RequestDescription description)
        {
            _service = service;
            _description = description;
            _orderApplied = false;
            bool useEF;
            var efEntitiesEncapsulatedDataService = service.FrameworkObject as IEFEntitiesEncapsulatedDataService;
            if (efEntitiesEncapsulatedDataService == null)
                useEF = false;
            else
            {
                useEF = true;
                _dataSource = efEntitiesEncapsulatedDataService.CurrentDataSource;
                var prevSegmentInfo = description.SegmentInfos[0];
                _efObjectQuery = efEntitiesEncapsulatedDataService.CurrentDataSource.GetEFQuery(prevSegmentInfo.Identifier);
                _query = _efObjectQuery;
                int nbSegments = _description.SegmentInfos.Length;
                var selectManyMethod = typeof(Queryable).GetMethods().Where(m => m.Name == "SelectMany").First();
                var selectMethod = typeof(Queryable).GetMethods().Where(m => m.Name == "Select").First();
                for (int indexSegmentInfo = 1; indexSegmentInfo < nbSegments; indexSegmentInfo++)
                {
                    var currentSegmentInfo = _description.SegmentInfos[indexSegmentInfo];
                    var currentSegmentProjectedProperty = currentSegmentInfo.ProjectedProperty;
                    PropertyInfo currentSegmentProjectedPropertyPropertyInfo;
                    EFEntityPropertyAttribute efEntityPropertyAttribute;
                    PropertyInfo efPropertyInfo;
                    if (currentSegmentProjectedProperty == null ||
                        (currentSegmentProjectedPropertyPropertyInfo = currentSegmentProjectedProperty.PropertyInfo) == null ||
                        (efEntityPropertyAttribute = currentSegmentProjectedPropertyPropertyInfo.Attribute<EFEntityPropertyAttribute>()) == null ||
                        (efPropertyInfo = GetEFPropertyInfo(_efObjectQuery, efEntityPropertyAttribute, currentSegmentProjectedPropertyPropertyInfo.Name)) == null)
                    {
                        useEF = false;
                        break;
                    }

                    if (!(useEF = AddWhereForKeys(useEF, prevSegmentInfo.Key, _service.Provider.GetResourceType(currentSegmentProjectedPropertyPropertyInfo.DeclaringType).KeyProperties)))
                        break;

                    if (efPropertyInfo.PropertyType.IsGenericType && efPropertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(EntityCollection<>))
                    {
                        var efPropertyInfoTypeOfEntityCollection = efPropertyInfo.PropertyType.GetGenericArguments()[0];
                        var selectManyParam = Expression.Parameter(_query.ElementType, "smp");
                        _efObjectQuery = (ObjectQuery)selectManyMethod.MakeGenericMethod(_query.ElementType, efPropertyInfoTypeOfEntityCollection).Invoke(null, new object[] { _efObjectQuery, Expression.Lambda(Expression.Convert(Expression.Property(selectManyParam, efPropertyInfo), typeof(IEnumerable<>).MakeGenericType(efPropertyInfoTypeOfEntityCollection)), selectManyParam) });
                    }
                    else
                    {
                        var selectParam = Expression.Parameter(_query.ElementType, "sp");
                        _efObjectQuery = (ObjectQuery)selectMethod.MakeGenericMethod(_query.ElementType, efPropertyInfo.PropertyType).Invoke(null, new object[] { _efObjectQuery, Expression.Lambda(Expression.Property(selectParam, efPropertyInfo), selectParam) });
                    }

                    prevSegmentInfo = currentSegmentInfo;
                    _query = _efObjectQuery;
                }
                if (prevSegmentInfo.Key != null)
                    useEF = AddWhereForKeys(useEF, prevSegmentInfo.Key, _service.Provider.GetResourceType(prevSegmentInfo.TargetElementType).KeyProperties);
            }
            if (!useEF)
            {
                _efObjectQuery = null;
                _query = (IQueryable)description.RequestEnumerable;
            }
            _filterQueryApplicable = (description.TargetKind == RequestTargetKind.Resource) || (description.TargetKind == RequestTargetKind.ComplexObject);
            _setQueryApplicable = (description.TargetKind == RequestTargetKind.Resource) && !description.IsSingleResult;
            _expandPaths = null;
            _expandPathsAsText = null;
            _queryResults = null;

            _requestQueryProcessor = ReflectionHelper.CreateInstance(FrameworkTypeName, service.FrameworkObject, description.FrameworkObject);
        }

        public object FrameworkObject
        {
            get { return _requestQueryProcessor; }
        }

        private bool AddWhereForKeys(bool useEF, KeyInstance keyInstance, List<ResourceProperty> keyProperties)
        {
            if (keyInstance.PositionalValues == null)
                return true;
            int nbKeyProperties = keyProperties.Count;
            var whereMethod = typeof(Queryable).GetMethods().Where(m => m.Name == "Where").First();
            for (int indexKeyProperty = 0; indexKeyProperty < nbKeyProperties; indexKeyProperty++)
            {
                PropertyInfo keyPropertyPropertyInfo;
                EFEntityPropertyAttribute keyPropertyEFEntityPropertyAttribute;
                PropertyInfo efKeyPropertyInfo;
                if ((keyPropertyPropertyInfo = keyProperties[indexKeyProperty].PropertyInfo) == null ||
                    (keyPropertyEFEntityPropertyAttribute = keyPropertyPropertyInfo.Attribute<EFEntityPropertyAttribute>()) == null ||
                    (efKeyPropertyInfo = GetEFPropertyInfo(_efObjectQuery, keyPropertyEFEntityPropertyAttribute, keyPropertyPropertyInfo.Name)) == null)
                {
                    useEF = false;
                    break;
                }
                var whereParam = Expression.Parameter(_query.ElementType, "wp");
                _efObjectQuery = (ObjectQuery)whereMethod.MakeGenericMethod(_query.ElementType).Invoke(null, new object[] { _efObjectQuery, Expression.Lambda(Expression.Equal(Expression.Property(whereParam, efKeyPropertyInfo), Expression.Constant(keyInstance.PositionalValues[indexKeyProperty])), whereParam) });
            }
            return useEF;
        }

        private PropertyInfo GetEFPropertyInfo(ObjectQuery oq, EFEntityPropertyAttribute efPropertyAttribute, string defaultName)
        {
            return (_efObjectQuery as IQueryable).ElementType.GetProperty(efPropertyAttribute.EFPropertyName ?? defaultName);
        }

        public static void CheckEmptyQueryArguments(IDataService service)
        {
            IDataServiceHost host = service.Host;
            if ((!string.IsNullOrEmpty(host.GetQueryStringItem("$expand")) || !string.IsNullOrEmpty(host.GetQueryStringItem("$filter"))) || ((!string.IsNullOrEmpty(host.GetQueryStringItem("$orderby")) || !string.IsNullOrEmpty(host.GetQueryStringItem("$skip"))) || !string.IsNullOrEmpty(host.GetQueryStringItem("$top"))))
                DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QueryNoOptionsApplicable).Throw();
        }

        internal static Expression ComposePropertyNavigation(Expression expression, LambdaExpression filterLambda, bool propagateNull)
        {
            Expression expression3;
            Expression right = ParameterReplacerVisitor.Replace(filterLambda.Body, filterLambda.Parameters[0], expression);
            if (propagateNull)
            {
                Expression nullLiteral = RequestQueryParser.NullLiteral;
                expression3 = Expression.AndAlso(Expression.NotEqual(expression, nullLiteral), right);
            }
            else
                expression3 = right;
            Expression ifTrue = expression;
            Expression ifFalse = Expression.Constant(null, ifTrue.Type);
            return Expression.Condition(expression3, ifTrue, ifFalse);
        }

        internal static RequestDescription ProcessQuery(IDataService service, RequestDescription description)
        {
            if (!(description.RequestEnumerable is IQueryable))
            {
                CheckEmptyQueryArguments(service);
                return description;
            }
            RequestQueryProcessor processor = new RequestQueryProcessor(service, description);
            return processor.ProcessQuery();
        }

        private static IQueryable InvokeOrderBy<TSource, TKey>(IQueryable source, LambdaExpression keySelector, bool ascending, bool firstLevelOrder)
        {
            Expression<Func<TSource, TKey>> expression = (Expression<Func<TSource, TKey>>)keySelector;
            if (firstLevelOrder)
            {
                IQueryable<TSource> queryable = (IQueryable<TSource>)source;
                if (ascending)
                    return queryable.OrderBy<TSource, TKey>(expression);
                return queryable.OrderByDescending<TSource, TKey>(expression);
            }
            IOrderedQueryable<TSource> queryable2 = (IOrderedQueryable<TSource>)source;
            if (ascending)
                return queryable2.ThenBy<TSource, TKey>(expression);
            return queryable2.ThenByDescending<TSource, TKey>(expression);
        }

        private static IQueryable InvokeSkip<T>(IQueryable source, int count)
        {
            IQueryable<T> queryable = (IQueryable<T>)source;
            return queryable.Skip<T>(count);
        }

        private static IQueryable InvokeTake<T>(IQueryable source, int count)
        {
            IQueryable<T> queryable = (IQueryable<T>)source;
            return queryable.Take<T>(count);
        }

        private static IQueryable OrderBy(IQueryable query, PropertyInfo property, bool ascending, bool firstLevelOrder)
        {
            ParameterExpression expression = Expression.Parameter(query.ElementType, "element");
            MemberExpression body = Expression.Property(expression, property);
            LambdaExpression expression3 = Expression.Lambda(body, new ParameterExpression[] { expression });
            return (IQueryable)typeof(RequestQueryProcessor).GetMethod("InvokeOrderBy", BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(new Type[] { query.ElementType, body.Type }).Invoke(null, new object[] { query, expression3, ascending, firstLevelOrder });
        }

        private static List<List<string>> ReadExpand(string expand)
        {
            return ReflectionHelper.InvokeStaticMethod<List<List<string>>>(FrameworkTypeName, "ReadExpand", expand);
        }

        private static IQueryable Skip(IQueryable query, int count)
        {
            return (IQueryable)ReflectionHelper.GetFrameworkType(FrameworkTypeName).GetMethod("InvokeSkip", BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(new Type[] { query.ElementType }).Invoke(null, new object[] { query, count });
        }

        private static IQueryable Take(IQueryable query, int count)
        {
            return (IQueryable)ReflectionHelper.GetFrameworkType(FrameworkTypeName).GetMethod("InvokeTake", BindingFlags.NonPublic | BindingFlags.Static).MakeGenericMethod(new Type[] { query.ElementType }).Invoke(null, new object[] { query, count });
        }

        private void ApplyDefaultOrder()
        {
            if ((_description.TargetKind == RequestTargetKind.Resource) && ((_description.TargetSource == RequestTargetSource.EntitySet) || (_description.TargetSource == RequestTargetSource.Property)))
            {
                ResourceType resourceType = _service.Provider.GetResourceType(_description.TargetElementType);
                bool firstLevelOrder = true;
                foreach (ResourceProperty property in resourceType.KeyProperties)
                {
                    if (_query.ElementType == resourceType.Type)
                        _query = OrderBy(_query, property.PropertyInfo, true, firstLevelOrder);
                    else
                    {
                        var efEntityPropertyAttribute = property.PropertyInfo.Attribute<EFEntityPropertyAttribute>();
                        _query = OrderBy(_query, _query.ElementType.GetProperty(efEntityPropertyAttribute.EFPropertyName ?? property.PropertyInfo.Name), true, firstLevelOrder); // I consider that the keys have always one EFEntityPropertyAttribute
                    }
                    firstLevelOrder = false;
                }
            }
            _orderApplied = true;
        }

        private void CheckExpandPaths()
        {
            if ((_expandPathsAsText.Count > 0) && (_query == null))
                DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QueryExpandOptionNotApplicable).Throw();
            _expandPaths = new List<ExpandSegmentCollection>(_expandPathsAsText.Count);
            for (int i = _expandPathsAsText.Count - 1; i >= 0; i--)
            {
                Type elementType = _description.TargetElementType;
                ResourceType resourceType = _service.Provider.GetResourceType(elementType);
                List<string> list = _expandPathsAsText[i];
                ExpandSegmentCollection item = new ExpandSegmentCollection(list.Count);
                bool flag = false;
                for (int j = 0; j < list.Count; j++)
                {
                    string propertyName = list[j];
                    ResourceProperty property = resourceType.TryResolvePropertyName(propertyName);
                    if (property == null)
                        DataServiceException.CreateSyntaxError(Strings.RequestUriProcessor_PropertyNotFound(resourceType.FullName, propertyName)).Throw();
                    Expression filter = null;
                    if (property.ResourceContainer != null)
                    {
                        bool singleResult = property.Kind == ResourcePropertyKind.ResourceReference;
                        _service.Configuration.CheckResourceRightsForRead(property.ResourceContainer, singleResult);
                        filter = DataServiceConfiguration.ComposeQueryInterceptors(_service, property.ResourceContainer);
                    }
                    int maxResultsPerCollection = _service.Configuration.MaxResultsPerCollection;
                    item.Add(new ExpandSegment(property.Name, filter, maxResultsPerCollection, property.ResourceContainer).FrameworkObject);
                    resourceType = property.ResourceType;
                    flag = flag || (property.TypeKind != ResourceTypeKind.EntityType);
                }
                if (flag)
                    _expandPathsAsText.RemoveAt(i);
                else
                    _expandPaths.Add(item);
            }
        }

        private void CheckExpandUpdatedPaths()
        {
            for (int i = _expandPaths.Count - 1; i >= 0; i--)
            {
                Type elementType = _query.ElementType;
                ResourceType resourceType = _service.Provider.GetResourceType(elementType);
                ExpandSegmentCollection segments = _expandPaths[i];
                for (int j = 0; j < segments.Count; j++)
                {
                    string name = segments[j].Name;
                    ResourceProperty property = resourceType.TryResolvePropertyName(name);
                    if (property == null)
                        DataServiceException.CreateSyntaxError(Strings.RequestUriProcessor_PropertyNotFound(resourceType.FullName, name)).Throw();
                    resourceType = property.ResourceType;
                }
            }
        }

        private void CheckFilterQueryApplicable()
        {
            if (!_filterQueryApplicable)
                DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QueryFilterOptionNotApplicable).Throw();
        }

        private void CheckSetQueryApplicable()
        {
            if (!_setQueryApplicable)
                DataServiceException.CreateBadRequestError(Strings.RequestQueryProcessor_QuerySetOptionsNotApplicable).Throw();
        }

        private void ProcessInclude()
        {
            FillExpandPaths();
            foreach (var path in _expandPaths)
            {
                var type = _description.TargetElementType;
                var includePath = GetIncludeName(ref type, path[0].Name);
                if (includePath == null)
                    continue;
                int count = path.Count;
                var included = new List<string>();
                IncludePath(type, includePath, included);
                for (int index = 1; index < count; index++)
                {
                    var includePathTmp = GetIncludeName(ref type, path[index].Name);
                    if (includePathTmp == null)
                        break;
                    includePath += "." + includePathTmp;
                    IncludePath(type, includePath, included);
                }
                IncludePath(includePath, included);
            }
        }

        private void IncludePath(Type type, string currentPath, List<string> included)
        {
            var efEntityIncludeAttribute = type.Attribute<EFEntityIncludeAttribute>();
            if (efEntityIncludeAttribute != null)
                IncludePath(string.Format("{0}.{1}", currentPath, efEntityIncludeAttribute.EFEntityIncludePropertyName), included);
        }

        private void IncludePath(string includePath, List<string> included)
        {
            if (!included.Contains(includePath))
            {
                included.Add(includePath);
                IQueryable query = _efObjectQuery;
                _efObjectQuery = ReflectionHelper.InvokeMethod<ObjectQuery>(typeof(ObjectQuery<>).MakeGenericType(_query.ElementType).GetMethod("Include"), _efObjectQuery, includePath);
            }
        }

        private string GetIncludeName(ref Type type, string pathName)
        {
            PropertyInfo pi = ReflectionHelper.GetInstancePropertyInfo(type, pathName);
            var efEntityPropertyAttribute = pi.Attribute<EFEntityPropertyAttribute>();
            if (efEntityPropertyAttribute == null)
                return null;
            type = pi.PropertyType;
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
                type = type.GetGenericArguments()[0];
            return efEntityPropertyAttribute.EFPropertyName ?? pathName;
        }

        private void ProcessExpand()
        {
            _service.InternalApplyingExpansions(_query, _expandPaths);
            CheckExpandUpdatedPaths();
        }

        private void FillExpandPaths()
        {
            string queryStringItem = _service.Host.GetQueryStringItem("$expand");
            if (!string.IsNullOrEmpty(queryStringItem))
                _expandPathsAsText = ReadExpand(queryStringItem);
            else
                _expandPathsAsText = new List<List<string>>();
            CheckExpandPaths();
        }

        private DoneTier ProcessFilter()
        {
            DoneTier dt = DoneTier.DoneInDB;
            string queryStringItem = _service.Host.GetQueryStringItem("$filter");
            if (!string.IsNullOrEmpty(queryStringItem))
            {
                CheckFilterQueryApplicable();
                _query = RequestQueryParser.Where(_service, _query, queryStringItem, _description, out dt);
            }
            return dt;
        }

        private DoneTier ProcessOrderBy()
        {
            DoneTier dt = DoneTier.DoneInDB;
            string queryStringItem = _service.Host.GetQueryStringItem("$orderby");
            if (!string.IsNullOrEmpty(queryStringItem))
            {
                CheckSetQueryApplicable();
                IQueryable queryable = RequestQueryParser.OrderBy(_service, _query, queryStringItem, _description, out dt);
                if (queryable != _query)
                {
                    _query = queryable;
                    _orderApplied = true;
                }
            }
            return dt;
        }

        private RequestDescription ProcessQuery()
        {
            if (_efObjectQuery == null)
            {
                var frameworkValue = ReflectionHelper.InvokeMethod(FrameworkTypeName, "ProcessQuery", _requestQueryProcessor);
                if (frameworkValue == null)
                    return null;
                return new RequestDescription(frameworkValue);
            }
            ProcessInclude();
            _query = _efObjectQuery;
            DoneTier doneTierFilter = ProcessFilter();
            if (doneTierFilter == DoneTier.DoneInServer)
                _query = _efObjectQuery;
            DoneTier doneTierOrder = ProcessOrderBy();
            bool doPagingInDB = ((doneTierFilter | doneTierOrder) & DoneTier.DoneInServer) == 0;
            if (doPagingInDB)
            {
                ProcessSkip();
                ProcessTop();
            }
            var bllType = _description.SegmentInfos.Last().TargetElementType;
            if (_dataSource != null)
            {
                _efObjectQuery = _query as ObjectQuery;
                _query = _dataSource.EncapsulateIntoBLLEntityType(_efObjectQuery, bllType);
            }
            if (doneTierFilter != DoneTier.DoneInDB)
                ProcessFilter();
            if (doneTierOrder != DoneTier.DoneInDB)
                ProcessOrderBy();
            if (!doPagingInDB)
            {
                ProcessSkip();
                ProcessTop();
            }
            if (typeof(IEntityNeedRightToRead).IsAssignableFrom(bllType))
            {
                var whereMethod = typeof(Queryable).GetMethods().Where(m => m.Name == "Where").First();
                var whereParam = Expression.Parameter(_query.ElementType, "wp");
                _query = (IQueryable)whereMethod.MakeGenericMethod(bllType).Invoke(null, new object[] { _query, Expression.Lambda(Expression.Call(Expression.Convert(whereParam, typeof(IEntityNeedRightToRead)), typeof(IEntityNeedRightToRead).GetMethod("CanRead"), Expression.Constant(HttpContext.Current.User.Identity.Name)), whereParam)});
            }
            ProcessExpand();
            if (_expandPaths.Count > 0)
                _queryResults = _service.Provider.ApplyExpansions(_query, _expandPaths);
            else
                _queryResults = _query;
            return new RequestDescription(_description, _queryResults, _expandPaths);
        }

        private void ProcessSkip()
        {
            int num;
            if (ProcessSkipOrTopArgument("$skip", out num))
            {
                CheckSetQueryApplicable();
                _query = Skip(_query, num);
            }
        }

        private bool ProcessSkipOrTopArgument(string queryItem, out int count)
        {
            string queryStringItem = _service.Host.GetQueryStringItem(queryItem);
            if (string.IsNullOrEmpty(queryStringItem))
            {
                count = 0;
                return false;
            }
            if (!_orderApplied)
                ApplyDefaultOrder();
            if (!int.TryParse(queryStringItem, NumberStyles.Integer, CultureInfo.InvariantCulture, out count))
                DataServiceException.CreateSyntaxError(Strings.RequestQueryProcessor_IncorrectArgumentFormat(queryItem, queryStringItem)).Throw();
            return true;
        }

        private void ProcessTop()
        {
            int num;
            if (ProcessSkipOrTopArgument("$top", out num))
            {
                CheckSetQueryApplicable();
                _query = Take(_query, num);
            }
        }
    }
}

